Skip to content

fix(analytics): make organization timezone drive date-dimension bucketing (#1982)#2018

Merged
os-zhuang merged 1 commit into
mainfrom
fix/analytics-tz-bucketing
Jun 18, 2026
Merged

fix(analytics): make organization timezone drive date-dimension bucketing (#1982)#2018
os-zhuang merged 1 commit into
mainfrom
fix/analytics-tz-bucketing

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Problem

A follow-up to ADR-0053 Phase 2 (#1982) browser verification: setting a non-UTC organization timezone (localization manifest, #2006) did not change a date-bucketed analytics chart. The unit tests for the tz bucketing engine passed, but the result never reached the API — the classic "passes build / silent at runtime" gap.

Tracing it revealed three independent broken seams, all required for an org timezone to actually drive a date bucket:

  1. @objectstack/service-analytics — wrong strategy won. NativeSQLStrategy (priority 10) handled every cube/dataset query on a SQL driver, but it groups by the raw column (GROUP BY <col>, no date_trunc) and references timezone nowhere. So date dimensions never bucketed (one row per raw timestamp) and a non-UTC zone was silently dropped. It now declines queries carrying a timeDimensions[].granularity, handing them to the lower-priority ObjectQLStrategyengine.aggregate (native bucketing when UTC-safe, uniform in-memory bucketing when non-UTC).

  2. @objectstack/objectql — count-all returned 0 in-memory. The Cube count measure and a fieldless dataset count both compile to sql: '*'. The in-memory count branch treated '*' as a column name and counted non-null of a non-existent property → 0 for every bucket. The driver's COUNT(*) masked it, but the in-memory path (non-UTC date buckets, driver-rest/driver-memory) returned zeros. '*' is now counted as all rows.

  3. @objectstack/rest — context had no timezone. resolveExecCtx never resolved the localization timezone/locale, so /analytics/dataset/query always ran with timezone: 'UTC'. It now resolves them through the settings service (honouring the 4-tier cascade incl. the OS_LOCALIZATION_TIMEZONE env override), mirroring the dispatcher path's resolveLocalization.

Verification

End-to-end against example-crm with a lead created at …T03:5x:…Z (UTC day 06-18, LA day 06-17):

Scenario Before After
org tz = UTC (settings) 06-18 cnt 5 06-18 cnt 5
org tz = America/Los_Angeles (settings) 06-18 cnt 5 ❌ 06-17 cnt 5
explicit selection.timezone=America/LA 06-17×… cnt 0 ❌ (uncollapsed) 06-17 cnt 5

Tests

  • New regression native-sql-granularity-decline.test.ts — pins the production capability shape (nativeSql: true AND objectqlAggregate: true); a granularity query must route to executeAggregate, not raw SQL. (The pre-existing granularity test forced nativeSql:false, which masked the bug.)
  • New in-memory-aggregation cases for the '*' count-all sentinel (ungrouped + per tz bucket).
  • Full suites green: objectql 660, service-analytics 127, rest 121.

🤖 Generated with Claude Code

…ting (#1982)

Date-bucketed analytics silently ignored the reference timezone end-to-end.
Three independent seams were broken; all three are needed for an org's
localization timezone to actually change a date-bucketed chart:

1. service-analytics — NativeSQLStrategy (priority 10) won every cube/dataset
   query on a SQL driver, but it groups by the raw column (no date_trunc) and
   ignores `timezone`, so date dimensions never bucketed (one row per raw
   timestamp) and a non-UTC zone was dropped. It now declines queries carrying
   a timeDimensions[].granularity, handing them to ObjectQLStrategy →
   engine.aggregate (native bucketing UTC-safe, uniform in-memory when non-UTC).

2. objectql — the in-memory `count` aggregation treated the `*` count-all
   sentinel (Cube `count` measure / fieldless dataset `count`, both compiled to
   sql:'*') as a column name → counted 0 for every bucket. The driver COUNT(*)
   masked it; the in-memory path (non-UTC date buckets, driver-rest/-memory)
   returned zeros. `*` is now counted as all rows.

3. rest — resolveExecCtx never resolved the localization timezone/locale, so
   /analytics/dataset/query always ran with timezone:'UTC'. It now resolves
   them through the settings service (4-tier cascade incl. the
   OS_LOCALIZATION_TIMEZONE env override), mirroring the dispatcher path.

Verified end-to-end against example-crm: an org tz of America/Los_Angeles now
buckets a 03:5x UTC lead into 2026-06-17 (cnt 5) vs UTC's 2026-06-18 (cnt 5).
Regression tests added for seams 1 and 2; full suites green
(objectql 660, service-analytics 127, rest 121).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 18, 2026 4:08am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jun 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/rest, packages/services.

19 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/core/services.mdx (via @objectstack/objectql)
  • content/docs/concepts/implementation-status.mdx (via @objectstack/objectql, @objectstack/rest)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/concepts/packages.mdx (via @objectstack/objectql, @objectstack/rest)
  • content/docs/guides/api-reference.mdx (via @objectstack/rest)
  • content/docs/guides/authentication.mdx (via @objectstack/objectql)
  • content/docs/guides/deployment-vercel.mdx (via @objectstack/objectql)
  • content/docs/guides/formula.mdx (via packages/objectql)
  • content/docs/guides/kernel-services.mdx (via @objectstack/objectql)
  • content/docs/guides/objectql-migration.mdx (via @objectstack/objectql)
  • content/docs/guides/packages.mdx (via @objectstack/objectql, @objectstack/rest, packages/services)
  • content/docs/guides/plugins.mdx (via @objectstack/objectql, @objectstack/rest)
  • content/docs/guides/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/guides/runtime-services/index.mdx (via packages/services)
  • content/docs/guides/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/protocol/objectos/i18n-standard.mdx (via packages/services)
  • content/docs/protocol/objectos/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/v9.mdx (via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit fd07027 into main Jun 18, 2026
14 checks passed
@os-zhuang
os-zhuang deleted the fix/analytics-tz-bucketing branch June 18, 2026 04:55
os-zhuang added a commit that referenced this pull request Jun 18, 2026
Static gates (build, unit tests, spec-liveness, CodeQL) verify each layer in
isolation, usually against mocks, and cannot catch a break that only appears
when the real engine + strategies + services + HTTP context run together. The
#2018 tz-bucketing regression is the canonical case: green on every static gate
(900+ unit tests included) because each of its three broken seams was
individually correct and individually mocked.

This adds `@objectstack/dogfood` (private), a harness that boots real example
apps in-process against in-memory SQLite — wired with the same service plugins
`objectstack dev` loads — and exercises them through the real HTTP surface via
Hono request-injection (no ports, no sockets, CI-stable). Tests act as a browser
client: sign in, hit /api/v1/..., assert on real responses.

- src/harness.ts — bootDogfoodStack(config) → { kernel, api, raw, signIn, apiAs, stop }
- test/analytics-timezone.dogfood.test.ts — golden regression for #1982/#2018:
  org timezone (set via the real settings route) must shift a boundary-crossing
  instant's date bucket (UTC 2024-03-01 → America/Los_Angeles 2024-02-29) with
  the correct count. Verified to FAIL when any #2018 seam is reverted.
- CI: new "Dogfood Regression Gate" job (also runs under turbo run test).

Boot-to-assert ~2s. Private package — no changeset.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jun 18, 2026
…2024)

* docs(adr): ADR-0054 prove-it-runs gate for the authorable surface

Extends ADR-0049 (enforce-or-remove) with a third leg. The liveness ledger
(#1919) classifies every authorable property live/experimental/dead, but "live"
means only a static file:line consumer pointer — proof that something reads the
property, not that authoring it produces correct runtime behavior. #2018 (tz
bucketing: live at every layer, broken in integration) and the field-type
fidelity gaps (#2022: rating/slider/toggle read back wrong-typed) fell through
that gap — call it "unproven liveness".

For a platform whose authors are AI emitting metadata across a combinatorial
space the examples never cover, unproven liveness ships silently into
third-party apps. ADR-0054 upgrades a `live` classification to optionally carry a
`proof` — a @objectstack/dogfood test that authors the property against the real
in-process stack and asserts the runtime outcome. Required as a ratchet (not a
retrofit) for a high-risk authorable class on change, and for any property
implicated in a shipped regression (the fix carries its proof). Generative
testing is explicitly deferred (Phase 3, evidence-gated).

Proposed — for architect review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(adr): accept ADR-0054 (prove-it-runs gate)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
os-zhuang pushed a commit that referenced this pull request Jun 19, 2026
…0054)

Unlocks the analytics binding that was previously blocked because its authorable
surface wasn't governed.

- Govern the `dataset` metadata type: new liveness/dataset.json classifies all 19
  authorable props with file:line evidence from the analytics service consumer
  audit (26 live, 1 dead — measures.certified has no runtime consumer). `dataset`
  added to GOVERNED in check-liveness.mts.
- Bind the analytics class: dataset.dimensions.dateGranularity carries the
  tz-bucketing proof (#1982/#2018) — the property whose org-timezone day-bucket
  behavior the proof asserts. proof-registry.mts flips analytics to bound.
- Four high-risk classes now CI-enforced: field types, RLS, flow nodes, analytics.
- README governed-types + high-risk tables updated; changeset description updated.

Verified: liveness gate green (dataset 27 classified); 17 registry tests; the
analytics proof runs green; full dogfood suite 59/59.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVdnfUAx85amkerym26vdx
os-zhuang added a commit that referenced this pull request Jun 19, 2026
…h-risk classes (ADR-0054 #1 + Phase 2) (#2045)

* feat(spec): add prove-it-runs proof field + ratchet to liveness gate (ADR-0054 #1)

ADR-0054 follow-up (1): the liveness ledger's `live` meant only a static
consumer pointer — necessary but not sufficient, since a property can be live at
every layer yet broken end-to-end. This adds the third leg: high-risk authorable
properties must carry a `proof` (a dogfood test reference) that asserts the
runtime outcome.

- proof-registry.mts: the authoritative high-risk-class list (field types,
  analytics, RLS, flow nodes, form widgets) + which classes the ratchet enforces
  this phase. Field types and RLS are bound (matrix exists AND surface governed);
  analytics/flow/form are listed-but-blocked with honest reasons (their surface
  isn't governed yet / no proof yet — Phase 2).
- check-liveness.mts: a bound `live` entry must carry a valid `proof` of its own
  class. Validation is STATIC (file exists + declares the `@proof: <id>` tag) so
  the gate stays seconds-cheap; running the proof remains the dogfood gate's job.
  Reverse check flags unregistered `@proof:` tags.
- Ledger: field.type → field-zoo proof; permission.rowLevelSecurity.using →
  #1994 RLS proof. Dogfood proofs self-declare their `@proof:` tag.
- Gate now also triggers on packages/dogfood/** so deleting/renaming a proof
  re-runs the check and the dangling reference is caught.
- 15 unit + wiring tests; README documents the contract and ratchet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVdnfUAx85amkerym26vdx

* chore: add changeset for the liveness prove-it-runs gate change

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVdnfUAx85amkerym26vdx

* feat(verify,spec): bind flow-node high-risk class with a runtime proof (ADR-0054 Phase 2)

Phase 2 extends the prove-it-runs ratchet to the flow-node class.

- verify: bootStack gains an opt-in `automation` flag that registers
  @objectstack/service-automation, so authored flows are pulled from the registry
  and POST /automation/:name/trigger runs their nodes. Without it, flow execution
  was unreachable through the harness (the dispatcher's automation routes resolved
  no service). Mirrors the existing `multiTenant`/`security` opt-ins; default off.
- dogfood: a self-contained flow fixture (one object + one autolaunched flow whose
  update_record node stamps a record) + the flow-node proof. It authors the flow,
  triggers it over HTTP, and asserts both directions — the targeted record is
  stamped (node executed) AND a bystander is untouched (the input variable wired
  into the node filter, not a blanket update). Runs green end-to-end.
- spec: flow-node class is now `bound` in proof-registry.mts; flow.nodes.type
  carries the proof; the liveness gate enforces it. Three classes now bound:
  field types, RLS, flow nodes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVdnfUAx85amkerym26vdx

* feat(spec): govern dataset + bind the analytics high-risk class (ADR-0054)

Unlocks the analytics binding that was previously blocked because its authorable
surface wasn't governed.

- Govern the `dataset` metadata type: new liveness/dataset.json classifies all 19
  authorable props with file:line evidence from the analytics service consumer
  audit (26 live, 1 dead — measures.certified has no runtime consumer). `dataset`
  added to GOVERNED in check-liveness.mts.
- Bind the analytics class: dataset.dimensions.dateGranularity carries the
  tz-bucketing proof (#1982/#2018) — the property whose org-timezone day-bucket
  behavior the proof asserts. proof-registry.mts flips analytics to bound.
- Four high-risk classes now CI-enforced: field types, RLS, flow nodes, analytics.
- README governed-types + high-risk tables updated; changeset description updated.

Verified: liveness gate green (dataset 27 classified); 17 registry tests; the
analytics proof runs green; full dogfood suite 59/59.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVdnfUAx85amkerym26vdx

---------

Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jun 27, 2026
feat(console): "Edit with AI" — open the build agent pre-scoped to an app's package (#2018)

objectui@2021b5f4a89c1940b2b3aa751ce4e33fe971974a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant